/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { ArkMethod, Stmt, ArkAssignStmt, NumberType, ArkNormalBinopExpr } from "arkanalyzer";
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseMetaData, BaseChecker } from "../BaseChecker";
import { Rule, Defects, MatcherTypes, MatcherCallback, MethodMatcher, ClassMatcher } from '../../Index';
import { RuleListUtil } from "../../utils/common/DefectsList";
import { IssueReport } from "../../model/Defects";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoLossOfPrecisionCheck');
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/no-loss-of-precision.md",
    description: "This number literal will lose precision at runtime."
};

export class NoLossOfPrecisionCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private buildMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
    };
    public registerMatchers(): MatcherCallback[] {
        const matchBuildCb: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        };
        return [matchBuildCb];
    }
    public check = (targetMtd: ArkMethod) => {
        const severity = this.rule.alert ?? this.metaData.severity;
        const stmts = targetMtd.getBody()?.getCfg().getStmts() ?? [];
        stmts.forEach((stmt) => {
            if (stmt instanceof ArkAssignStmt) {
                const originalText = stmt.getOriginalText();
                if (!originalText) {
                    return;
                }
                const trimmedText = originalText.trim();
                const right = stmt.getRightOp();
                if (!trimmedText || (right instanceof ArkNormalBinopExpr)) {
                    return;
                }
                const getRightOpType = stmt.getRightOp()?.getType();
                if (getRightOpType instanceof NumberType) {
                    const parts = originalText.split('=');
                    if (parts.length < 2) {
                        return;
                    }
                    const originalTextValue = parts[1].trim().replace(/;$/, '');
                    const cleanedNumberStr = originalTextValue.replace(/_/g, '');
                    let numberStr = cleanedNumberStr.toString();
                    let numberValue: number;
                    if (numberStr.startsWith('0x') || numberStr.startsWith('0X')) {
                        numberValue = parseInt(numberStr, 16);
                    } else if (numberStr.startsWith('0o') || numberStr.startsWith('0O')) {
                        numberValue = +stmt.getRightOp();
                    } else if (numberStr.startsWith('0b') || numberStr.startsWith('0B')) {
                        numberValue = +stmt.getRightOp();
                    } else if (numberStr.includes('e') || numberStr.includes('E')) {
                        numberValue = parseFloat(numberStr);
                    } else {
                        numberValue = parseFloat(numberStr);
                    }
                    const [integerPart, decimalPart] = numberStr.split('.');
                    let isIntegerInRange = false;
                    if (originalTextValue.includes('e') || originalTextValue.includes('E')) {
                        isIntegerInRange = true;
                    }
                    if (!isIntegerInRange && (numberValue > Number.MAX_SAFE_INTEGER || numberValue < Number.MIN_SAFE_INTEGER)) {
                        const lastDigit = originalTextValue[originalTextValue.length - 1];
                        if (lastDigit === '0' && !originalTextValue.includes('.')) {
                            numberStr = stmt.getRightOp().toString();
                        } else {
                            this.addIssueReport(stmt, originalTextValue, severity);
                            return;
                        }
                    }
                    if (numberStr.includes('e') || numberStr.includes('E')) {
                        const [mantissa, exponent] = numberStr.split(/[eE]/);
                        const expNum = parseInt(exponent, 10);
                        if (expNum > 308 || expNum < -308) {
                            var value = originalText.split('=')[1].trim();
                            this.addIssueReport(stmt, value, severity);
                            return;
                        }
                        const [mantissaIntegerPart, mantissaDecimalPart] = mantissa.split('.');
                        if (mantissaDecimalPart && mantissaDecimalPart.length > 15) {
                            var value = originalText.split('=')[1].trim();
                            this.addIssueReport(stmt, value, severity);
                            return;
                        }
                    }
                    if (decimalPart) {
                        if (decimalPart.length > 15) {
                            const normalizedDecimalPart = decimalPart.replace(/0+$/, '');
                            if (normalizedDecimalPart.length > 1) {
                                var value = originalText.split('=')[1].trim();
                                this.addIssueReport(stmt, value, severity);
                                return;
                            }
                        } else {
                            const num = parseFloat(numberStr).toString();
                            if (num !== numberStr) {
                                var value = originalText.split('=')[1].trim();
                                this.addIssueReport(stmt, value, severity);
                                return;
                            }
                        }
                    }
                }
            }
        });
    };
    private addIssueReport(stmt: Stmt, name: string, severity: number) {
        const warnInfo = this.getLineAndColumn(stmt, name);
        const fixKey = warnInfo.line + '%' + warnInfo.startCol + '%' + warnInfo.endCol + '%' + this.rule.ruleId;
        const mergeKey = warnInfo.filePath + '%' + fixKey + '%' + this.metaData.description;
        const defects = (new Defects(warnInfo.line, warnInfo.startCol, this.metaData.description, severity, this.rule.ruleId,
            mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey));
        this.issues.push(new IssueReport(defects, undefined));
        RuleListUtil.push(defects);
    }
    private getLineAndColumn(stmt: Stmt, name: string) {
        const originPosition = stmt.getOriginPositionInfo();
        const line = originPosition.getLineNo();
        const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
        if (arkFile) {
            let originText = stmt.getOriginalText() ?? '';
            let startCol = originPosition.getColNo();
            const pos = originText.indexOf(name);
            if (pos !== -1) {
                startCol += pos;
                const endCol = startCol + name.length - 1;
                const originPath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath();
                return { line, startCol, endCol, filePath: originPath };
            }
        }
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }
}